package weka.classifiers.collective.meta;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Vector;

import weka.classifiers.AbstractClassifier;
import weka.classifiers.Classifier;
import weka.classifiers.collective.CollectiveRandomizableSingleClassifierEnhancer;
import weka.core.AdditionalMeasureProducer;
import weka.core.CapabilitiesHandler;
import weka.core.Instance;
import weka.core.Instances;
import weka.core.Option;
import weka.core.OptionHandler;
import weka.core.RevisionUtils;
import weka.core.Utils;
import weka.core.Capabilities.Capability;
import weka.core.SelectedTag;
import weka.core.Tag;

public class Hybrid extends CollectiveRandomizableSingleClassifierEnhancer implements
        OptionHandler,
        Serializable,
        AdditionalMeasureProducer,
        CapabilitiesHandler {

    /** Variable for storing the generated base classifier. */
    private Classifier classifierOne, classifierTwo, classifierTieBreaker;
    private double numUncertain;
    private double uncertaintyRatio;
    private double threshold;
    private int m_Combiner = 1;
    private static Tag[] TAGS_Combiner = {
        new Tag(1, "AVG", "Average (AVG)"),
        new Tag(2, "MV", "Majority Vote (MV)"),};

    public Hybrid() throws Exception {
        classifierOne = defaultClassifier();
        classifierTwo = defaultClassifier();
        classifierTieBreaker = defaultClassifierTieBreaker();
    }

    /**
     * String describing default classifier.
     */
    protected String defaultClassifierString() {

        return "weka.classifiers.meta.Bagging";
    }

    /**
     * String describing default classifier.
     */
    protected Classifier defaultClassifier() throws Exception {

        return new weka.classifiers.meta.Bagging();
    }

    public Classifier defaultClassifierTieBreaker() {
        return new weka.classifiers.lazy.IB1();
    }

    /**
     * Returns a string describing classifier
     * @return a description suitable for
     * displaying in the explorer/experimenter gui
     */
    public String globalInfo() {

        return "";
    }

    /**
     * Returns the revision string.
     *
     * @return		the revision
     */
    public String getRevision() {
        return RevisionUtils.extract("$Revision: 0.1 $");
    }

    @Override
    protected double[] getDistribution(Instance instance) throws Exception {
        if (threshold == 0) {
            return getDistribution(instance, this);
        }

        int[] result = getCertainClass(instance);
        if (result[0] == -1) {
            return classifierTieBreaker.distributionForInstance(instance);
        } else if (result[1] == 1) {
            return getDistribution(instance, classifierOne);
        } else if (result[1] == 2) {
            return getDistribution(instance, classifierTwo);
        } else {
            return getDistribution(instance, this);
        }
    }

    private double[] getDistribution(Instance instance, Classifier classifier) throws Exception {
        double[] result = new double[instance.numClasses()];
        if (getCombiner().getSelectedTag().getID() == 1) {
            if (classifier.isEnsemble()) {
                double[] dist;
                for (Classifier base : classifier.getEnsembleMembers()) {
                    dist = base.distributionForInstance(instance);
                    for (int i = 0; i < instance.numClasses(); i++) {
                        result[i] += dist[i];
                    }
                }
                for (int i = 0; i < instance.numClasses(); i++) {
                    result[i] /= classifier.getEnsembleMembers().size();
                }
            } else {
                result = classifier.distributionForInstance(instance);
            }
        } else {
            if (classifier.isEnsemble()) {
                double pred;
                for (Classifier base : classifier.getEnsembleMembers()) {
                    pred = base.classifyInstance(instance);
                    result[(int) pred]++;
                }
                for (int i = 0; i < instance.numClasses(); i++) {
                    result[i] /= classifier.getEnsembleMembers().size();
                }
            } else {
                double pred = classifier.classifyInstance(instance);
                result[(int) pred]++;
            }
        }
        return result;
    }

    @Override
    protected void buildClassifier() throws Exception {
        //double xWeight = (m_Trainset.numInstances() + 0.0) / (m_Testset.numInstances() + m_Trainset.numInstances() + 0.0);

        Instances newData = new Instances(m_Trainset);
        classifierOne.buildClassifier(newData);
        classifierTwo.buildClassifier(newData);
        classifierTieBreaker.buildClassifier(newData);

        calcNumUncertain(m_TestsetOriginal);
        calcUncertaintyRatio(m_TestsetOriginal);

    }

    @Override
    protected void build() throws Exception {
        buildClassifier();
    }

    /**
     * Returns an enumeration describing the available options
     *
     * @return an enumeration of all the available options
     */
    public Enumeration listOptions() {

        Vector newVector = new Vector(3);

        newVector.addElement(new Option(
                "\tFull name of classifier one.\n" + "\t(default: " + defaultClassifierString() + ")",
                "A", 1, "-A"));
        newVector.addElement(new Option(
                "",
                "", 0, "\nOptions specific to classifier " + classifierOne.getClass().getName() + ":"));
        Enumeration enu = ((OptionHandler) classifierOne).listOptions();
        while (enu.hasMoreElements()) {
            newVector.addElement(enu.nextElement());
        }

        newVector.addElement(new Option(
                "\tFull name of classifier two.\n" + "\t(default: " + defaultClassifierString() + ")",
                "B", 1, "-B"));
        newVector.addElement(new Option(
                "",
                "", 0, "\nOptions specific to classifier " + classifierTwo.getClass().getName() + ":"));
        enu = ((OptionHandler) classifierTwo).listOptions();
        while (enu.hasMoreElements()) {
            newVector.addElement(enu.nextElement());
        }

        newVector.addElement(new Option(
                "\tFull name of TieBreaker classifier.\n" + "\t(default: " + defaultClassifierTieBreaker() + ")",
                "K", 1, "-K"));
        newVector.addElement(new Option(
                "",
                "", 0, "\nOptions specific to classifier " + classifierTieBreaker.getClass().getName() + ":"));
        enu = ((OptionHandler) classifierTieBreaker).listOptions();
        while (enu.hasMoreElements()) {
            newVector.addElement(enu.nextElement());
        }

        newVector.addElement(new Option("\tThe threshold used to determine when the tiebreaker is used. 0 means an ensemble of all included models.\n" + "\t(default: 0.5)",
                "T", 1, "-T"));

        newVector.addElement(new Option(CombinerTipText(), "C", 0, "-C"));

        return newVector.elements();
    }

    /**
     * Parses a given list of options. Valid options are:<p>
     *
     * -B classifierstring <br>
     * Classifierstring should contain the full class name of a scheme
     * included for selection followed by options to the classifier
     * (required, option should be used once for each classifier).<p>
     *
     * @param options the list of options as an array of strings
     * @exception Exception if an option is not supported
     */
    public void setOptions(String[] options) throws Exception {
        String classifierString = Utils.getOption('A', options);
        String[] classifierSpec = Utils.splitOptions(classifierString);
        String classifierName;
        if (classifierSpec.length == 0) {
            classifierName = defaultClassifierString();
        } else {
            classifierName = classifierSpec[0];
            classifierSpec[0] = "";
        }
        classifierOne = AbstractClassifier.forName(classifierName, classifierSpec);

        classifierString = Utils.getOption('B', options);
        classifierSpec = Utils.splitOptions(classifierString);
        if (classifierSpec.length == 0) {
            classifierName = defaultClassifierString();
        } else {
            classifierName = classifierSpec[0];
            classifierSpec[0] = "";
        }
        classifierTwo = AbstractClassifier.forName(classifierName, classifierSpec);

        classifierString = Utils.getOption('K', options);
        classifierSpec = Utils.splitOptions(classifierString);
        if (classifierSpec.length == 0) {
            classifierName = defaultClassifierString();
        } else {
            classifierName = classifierSpec[0];
            classifierSpec[0] = "";
        }
        classifierTieBreaker = AbstractClassifier.forName(classifierName, classifierSpec);

        classifierString = Utils.getOption('T', options);
        if (classifierString.length() != 0) {
            setThreshold(Double.parseDouble(classifierString));
        } else {
            setThreshold(0.5);
        }

        classifierString = Utils.getOption("C", options);
        if (classifierString.length() != 0) {
            setCombiner(new SelectedTag(classifierString, TAGS_Combiner));
        } else {
            setCombiner(new SelectedTag(m_Combiner, TAGS_Combiner));
        }
    }

    /**
     * Gets the current settings of the Classifier.
     *
     * @return an array of strings suitable for passing to setOptions
     */
    public String[] getOptions() {

        String[] options;
        Vector<String> result = new Vector<String>();

        result.add("-T");
        result.add(getThreshold() + "");

        result.add("-C");
        result.add("" + getCombiner());

        result.add("-A");
        result.add(classifierOne.getClass().getName() + " "
                + Utils.joinOptions(((OptionHandler) classifierOne).getOptions()));

        result.add("-B");
        result.add(classifierTwo.getClass().getName() + " "
                + Utils.joinOptions(((OptionHandler) classifierTwo).getOptions()));

        result.add("-K");
        result.add(classifierTieBreaker.getClass().getName() + " "
                + Utils.joinOptions(((OptionHandler) classifierTieBreaker).getOptions()));

        options = super.getOptions();
        for (int i = 0; i < options.length; i++) {
            result.add(options[i]);
        }

        String[] array = new String[result.size()];
        for (int i = 0; i < result.size(); i++) {
            array[i] = result.elementAt(i).toString();
        }

        return array; // (String[]) result.toArray(new String[result.size()]);
    }

    /**
     * Returns the tip text for this property
     * @return tip text for this property suitable for
     * displaying in the explorer/experimenter gui
     */
    public String ClassifierOneTipText() {
        return "The first classifier to be used.";
    }

    /**
     * Sets the base classifer.
     *
     * @param classifier a base classifier with all options set.
     */
    public void setClassifierOne(Classifier classifier) {

        classifierOne = classifier;
    }

    /**
     * Gets the base classifer.
     *
     * @return the Base Classifier
     */
    public Classifier getClassifierOne() {

        return classifierOne;
    }

    /**
     * Returns the tip text for this property
     * @return tip text for this property suitable for
     * displaying in the explorer/experimenter gui
     */
    public String ClassifierTwoTipText() {
        return "The lassifier to be used.";
    }

    /**
     * Sets the rule extraction classifer.
     *
     * @param classifier a rule extraction classifier with all options set.
     */
    public void setClassifierTwo(Classifier classifier) {

        classifierTwo = classifier;
    }

    /**
     * Gets the rule extraction classifer.
     *
     * @return the Rule Extraction Classifier
     */
    public Classifier getClassifierTwo() {

        return classifierTwo;
    }

    /**
     * Returns the tip text for this property
     * @return tip text for this property suitable for
     * displaying in the explorer/experimenter gui
     */
    public String ClassifierTieBreakerTipText() {
        return "The classifier to be used as tiebreaker.";
    }

    /**
     * Sets the rule extraction classifer.
     *
     * @param classifier a rule extraction classifier with all options set.
     */
    public void setClassifierTieBreaker(Classifier classifier) {

        classifierTieBreaker = classifier;
    }

    /**
     * Gets the rule extraction classifer.
     *
     * @return the Rule Extraction Classifier
     */
    public Classifier getClassifierTieBreaker() {

        return classifierTieBreaker;
    }

    /**
     * Returns the tip text for this property
     * @return tip text for this property suitable for
     * displaying in the explorer/experimenter gui
     */
    public String ThresholdTipText() {
        return "The threshold used to determine when the tiebreaker is used. 0 means an ensemble of all included models";
    }

    /**
     * Sets the rule extraction classifer.
     *
     * @param classifier a rule extraction classifier with all options set.
     */
    public void setThreshold(double value) {

        threshold = value;
    }

    /**
     * Gets the rule extraction classifer.
     *
     * @return the Rule Extraction Classifier
     */
    public double getThreshold() {

        return threshold;
    }

    /**
     * Returns the tip text for this property
     *
     * @return tip text for this property suitable for displaying in the
     *         explorer/experimenter gui
     */
    public String CombinerTipText() {
        return "Determines which combiner method to use to determine class probability.";
    }

    /**
     * Get the value of
     *
     * @return Value of Settings.
     */
    public SelectedTag getCombiner() {
        return new SelectedTag(m_Combiner, TAGS_Combiner);
    }

    /**
     * Set the value of Settings.
     *
     * @param bnfFile
     *            Value to assign to Settings.
     */
    public void setCombiner(SelectedTag value) {
        if (value.getTags() == TAGS_Combiner) {
            m_Combiner = value.getSelectedTag().getID();
        }
    }

    /**
     * Output a representation of this classifier
     */
    public String toString() {

        if (classifierOne == null) {
            return "No model built yet.";
        }

        if (classifierTwo == null) {
            return "No model built yet.";
        }

        String result = "classifier One is:\n";
        result += classifierOne.toString();
        result += "\nClassifier two is:\n";
        result += classifierTwo.toString();

        return result;
    }

    public static void main(String[] args) throws Exception {
        runClassifier(new Hybrid(), args);

    }

    public Enumeration enumerateMeasures() {
        Vector measures = new Vector();
        measures.add("measureOneAccuracy");
        measures.add("measureTwoAccuracy");
        measures.add("measureCombinedAccuracy");
        measures.add("measureNumUncertain");
        measures.add("measureUncertainPercent");

        return measures.elements();
    }

    public double getMeasure(String measureName) {
        try {
            if (measureName.equalsIgnoreCase("measureOneAccuracy")) {
                return getAccuracy(classifierOne);
            } else if (measureName.equalsIgnoreCase("measureTwoAccuracy")) {
                return getAccuracy(classifierTwo);
            } else if (measureName.equalsIgnoreCase("measureCombinedAccuracy")) {
                return getCombinedAccuracy();
            } else if (measureName.equalsIgnoreCase("measureNumUncertain")) {
                return getNumUncertain();
            } else if (measureName.equalsIgnoreCase("measureUncertainPercent")) {
                return getUncertainPercent();
            } else {
                return 0;
            }
        } catch (Exception e) {
            return 0;
        }
    }

    private double getAccuracy(Classifier classifier) throws Exception {
        double accuracy = 0;
        for (int i = 0; i < m_TestsetOriginal.numInstances(); i++) {
            Instance instance = m_TestsetOriginal.instance(i);
            if (classifier.classifyInstance(instance) == instance.classValue()) {
                accuracy++;
            }
        }
        return (accuracy / m_TestsetOriginal.numInstances()) * 100;
    }

    private double getCombinedAccuracy() throws Exception {
        double tmpThreshold = threshold;

        threshold = 0;
        double accuracy = 0;
        for (int i = 0; i < m_TestsetOriginal.numInstances(); i++) {
            Instance instance = m_TestsetOriginal.instance(i);
            if (this.classifyInstance(instance) == instance.classValue()) {
                accuracy++;
            }
        }

        threshold = tmpThreshold;
        return (accuracy / m_TestsetOriginal.numInstances()) * 100;
    }

    private double getNumUncertain() {
        return numUncertain;
    }

    private double getUncertainPercent() {
        return uncertaintyRatio * 100;
    }

    private void calcNumUncertain(Instances instances) throws Exception {
        numUncertain = 0;
        for (int i = 0; i < instances.numInstances(); i++) {
            if (isUncertain(instances.instance(i))) {
                numUncertain++;
            }
        }
    }

    private void calcUncertaintyRatio(Instances instances) throws Exception {
        if (numUncertain == -1) {
            calcNumUncertain(instances);
        }
        uncertaintyRatio = numUncertain / instances.numInstances();
    }

    private boolean isUncertain(Instance instance) throws Exception {
        if (getCertainClass(instance)[0] == -1) {
            return true;
        } else {
            return false;
        }
    }

    private boolean certain(double belief) {
        return belief > threshold;
    }

    private int[] getCertainClass(Instance instance) throws Exception {
        int[] result = new int[2];
        double[] oneDist = classifierOne.distributionForInstance(instance);
        double[] twoDist = classifierTwo.distributionForInstance(instance);

        int maxOne = 0, maxTwo = 0;
        double dOne = oneDist[0], dTwo = twoDist[0];

        for (int i = 1; i < oneDist.length; i++) {
            if (dOne < oneDist[i]) {
                dOne = oneDist[i];
                maxOne = i;
            }
            if (dTwo < twoDist[i]) {
                dTwo = twoDist[i];
                maxTwo = i;
            }
        }
        if (maxOne == maxTwo) {
            result[0] = maxOne;
            result[1] = 0;
        } else if (certain(dOne) && !certain(dTwo)) {
            result[0] = maxOne;
            result[1] = 1;
        } else if (!certain(dOne) && certain(dTwo)) {
            result[0] = maxTwo;
            result[1] = 2;
        } else {
            result[0] = -1;
            result[1] = 0;
        }
        return result;
    }

    @Override
    public boolean isEnsemble() {
        return true;
    }

    public List<Classifier> getEnsembleMembers() {
	List<Classifier> list = new ArrayList<Classifier>();
        if (classifierOne.isEnsemble())
            list = classifierOne.getEnsembleMembers();
        else
            list.add(classifierOne);

        if (classifierTwo.isEnsemble())
            list.addAll(classifierTwo.getEnsembleMembers());
        else
            list.add(classifierTwo);

        return list;
    }
}
